Subject: fix for vulnerability CVE-2012-0049 for OpenTTD 1.0.1 - 1.0.1 (Denial of service (server) via slow read attack)
From: OpenTTD developer team <info@openttd.org>
Origin: backport, https://github.com/OpenTTD/OpenTTD/commit/bddfcae
Bug: https://github.com/OpenTTD/OpenTTD/issues/4955

Using a slow read type attack it is possible to prevent anyone from joining 
a server with virtually no resources. Once downloading the map no other
downloads of the map can start, so downloading really slowly will prevent 
others from joining. This can be further aggravated by the pause-on-join
setting in which case the game is paused and the players cannot continue the 
game during such an attack. This attack requires that the user is not banned 
and passes the authorization to the server, although for many servers there 
is no server password and thus authorization is easy.

A similar attack can be done when performing the attack during the
authorization phase itself, however you will not block anyone else from
joining, unless you use connection multiple times until the connection limit 
is reached, or stop the continuation of the game of the already joined
players. This attack requires the user to be merely not banned.

Note that versions before 0.6.0 are vulnerable as well. However, these
versions are over five years old and not supported anymore. Therefore no
patches for earlier versions are provided. Before 0.3.5 it is not possible
to exploit this bug via the internet as multiplayer over internet did not 
exist yet. The provided patch is a simplification of the fix in 1.1.5
because that version slightly changes the network protocol to tell people 
they got kicked due to the (password) timeout.

The attached patch does not change network compatibility. The fix in trunk
does change network compatibility.


Index: src/table/settings.h
===================================================================
--- src/table/settings.h	(revision 19742)
+++ src/table/settings.h	(working copy)
@@ -610,6 +610,8 @@
 	  SDTC_VAR(network.sync_freq,            SLE_UINT16,C|S,NO,   100,        0,      100, 0, STR_NULL,                                       NULL),
 	  SDTC_VAR(network.frame_freq,            SLE_UINT8,C|S,NO,     0,        0,      100, 0, STR_NULL,                                       NULL),
 	  SDTC_VAR(network.max_join_time,        SLE_UINT16, S, NO,   500,        0,    32000, 0, STR_NULL,                                       NULL),
+	  SDTC_VAR(network.max_download_time,    SLE_UINT16, S, NO,  1000,        0,    32000, 0, STR_NULL,                                       NULL),
+	  SDTC_VAR(network.max_password_time,    SLE_UINT16, S, NO,  2000,        0,    32000, 0, STR_NULL,                                       NULL),
 	 SDTC_BOOL(network.pause_on_join,                    S, NO,  true,                        STR_NULL,                                       NULL),
 	  SDTC_VAR(network.server_port,          SLE_UINT16, S, NO,NETWORK_DEFAULT_PORT,0,65535,0,STR_NULL,                                       NULL),
 	 SDTC_BOOL(network.server_advertise,                 S, NO, false,                        STR_NULL,                                       NULL),
Index: src/settings_type.h
===================================================================
--- src/settings_type.h	(revision 19742)
+++ src/settings_type.h	(working copy)
@@ -125,6 +125,8 @@
 	uint16 sync_freq;                                     ///< how often do we check whether we are still in-sync
 	uint8  frame_freq;                                    ///< how often do we send commands to the clients
 	uint16 max_join_time;                                 ///< maximum amount of time, in game ticks, a client may take to join
+	uint16 max_download_time;                             ///< maximum amount of time, in game ticks, a client may take to download the map
+	uint16 max_password_time;                             ///< maximum amount of time, in game ticks, a client may take to enter the password
 	bool   pause_on_join;                                 ///< pause the game when people join
 	uint16 server_port;                                   ///< port the server listens on
 	char   server_name[NETWORK_NAME_LENGTH];              ///< name of the server
Index: src/network/network_server.cpp
===================================================================
--- src/network/network_server.cpp	(revision 19742)
+++ src/network/network_server.cpp	(working copy)
@@ -1656,18 +1656,44 @@
 			} else {
 				cs->lag_test = 0;
 			}
-		} else if (cs->status == STATUS_PRE_ACTIVE) {
+		} else if (cs->status == STATUS_DONE_MAP ||
+					cs->status == STATUS_PRE_ACTIVE) {
+			/* The map has been sent, so this is for loading the map and syncing up. */
 			int lag = NetworkCalculateLag(cs);
 			if (lag > _settings_client.network.max_join_time) {
 				IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks for him to join", cs->client_id, _settings_client.network.max_join_time);
 				NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR);
 			}
-		} else if (cs->status == STATUS_INACTIVE) {
+		} else if (cs->status == STATUS_INACTIVE ||
+				cs->status == STATUS_AUTHORIZED) {
+			/* NewGRF check and authorized states should be handled almost instantly.
+			 * So give them some lee-way, likewise for the query with inactive. */
 			int lag = NetworkCalculateLag(cs);
 			if (lag > 4 * DAY_TICKS) {
 				IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks to start the joining process", cs->client_id, 4 * DAY_TICKS);
 				NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR);
 			}
+		} else if (cs->status == STATUS_MAP) {
+			/* Downloading the map... this is the amount of time since starting the saving. */
+			uint lag = NetworkCalculateLag(cs);
+			if (lag > _settings_client.network.max_download_time) {
+				IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks for him to download the map", cs->client_id, _settings_client.network.max_download_time);
+				NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR);
+			}
+		} else if (cs->status == STATUS_AUTH_GAME ||
+				cs->status == STATUS_AUTH_COMPANY) {
+			/* Waiting for the password. */
+			uint lag = NetworkCalculateLag(cs);
+			if (lag > _settings_client.network.max_password_time) {
+				IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks to enter the password", cs->client_id, _settings_client.network.max_password_time);
+				NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR);
+			}
+		} else if (cs->status == STATUS_MAP_WAIT) {
+			/* This is an internal state where we do not wait
+			 * on the client to move to a different state. */
+		} else {
+			/* Bad server/code. */
+			NOT_REACHED();
 		}
 
 		if (cs->status >= STATUS_PRE_ACTIVE) {

